#!/usr/bin/env dart
// ignore_for_file: avoid_print, constant_identifier_names

import 'dart:convert';
import 'dart:io';

const HELP = '''
Usage: build_ci.dart -arg val
  Params: 
    -c, --channel: 渠道
    -n, --app-name: app名称
    -ld, --launch-app-devices: 安装并启动的设备的名称
    -p, --pkg-name: 包名
    -rd, --release-dir: 发布目录

  eg: build_ci.dart -c kaifa -n '企业端测试服-v6' -ld 'fjy' -p 'com.hyzh.enterprise_v6_app'
''';

late final String? channel;
late final String? appName;
late final List<String>? launchDevices;
late final String? pkgName;
late final String? releaseDir;

void main(List<String> args) async {
  if (args.contains('--help') || args.contains('-h')) {
    print(HELP);
    exit(0);
  }

  if (args.length % 2 != 0) {
    print(BotIface(error: '参数 %2 != 0'));
    exit(1);
  }

  final params = <String, String>{};
  for (var i = 0; i < args.length; i += 2) {
    params[args[i]] = args[i + 1];
  }

  channel = params['--channel'] ?? params['-c'];
  appName = params['--app-name'] ?? params['-n'];
  launchDevices = (params['--launch-app-devices'] ?? params['-ld'])?.split(',');
  pkgName = params['--pkg-name'] ?? params['-p'];
  releaseDir = params['--release-dir'] ?? params['-rd'];

  await buildApk;
}

/// This iface describes the json format of the bot response.
class BotIface {
  BotIface({this.results, this.error});

  final String? error;
  final Map<String, dynamic>? results;

  Map<String, dynamic> toJson() {
    if (error != null) {
      return {
        'error': error,
      };
    }
    return {
      'results': results,
    };
  }

  @override
  String toString() {
    return ENCODER.convert(toJson());
  }
}

Future<void> get buildApk async {
  final time = DateTime.now();
  final result = await Process.run(
    'flutter',
    [
      'build',
      'apk',
      '--release',
      if (channel != null) '--dart-define=APP_INSTALL_CHANNEL=$channel',
      if (appName != null) '--dart-define=APP_NAME=$appName',
    ],
    runInShell: true,
  );
  if (result.exitCode != 0) {
    print(BotIface(error: result.stderr.toString()));
    exit(1);
  }

  const apkPath = 'build/app/outputs/apk/release/app-release.apk';
  final apkFile = File(apkPath);
  if (!await apkFile.exists()) {
    print(BotIface(error: 'apk file not found: $apkPath'));
    exit(1);
  }

  final newFileName = '$appName-$channel-${time.millisecondsSinceEpoch / 1000}.apk';
  final newName = 'build/app/outputs/apk/release/$newFileName';
  try {
    await apkFile.rename(newName);
  } catch (e) {
    print(BotIface(error: 'rename failed: $e'));
    exit(1);
  }

  // copy to caddy server location
  if (releaseDir != null) {
    final result2 = await Process.run(
      'cp',
      [
        newName,
        releaseDir!,
      ],
      runInShell: true,
    );
    if (result2.exitCode != 0) {
      print(BotIface(error: result2.stderr.toString()));
      exit(1);
    }
  }

  final results = <String, dynamic>{};
  //results['构建时间'] = time.toIso8601String();
  results['应用名称'] = appName;
  //results['应用渠道'] = channel;
  results['构建耗时'] = '${DateTime.now().difference(time).inSeconds} 秒';
  final urlEncodedFileName = Uri.encodeComponent(newFileName);
  final url = 'http://${await lanIP}:8080/$urlEncodedFileName';
  results['内网下载'] = url;

  if (launchDevices != null) {
    final errs = <String>[];
    for (final device in launchDevices!) {
      final deviceId = adbIdNameMap[device];
      if (deviceId == null) {
        errs.add('device $device not found');
        continue;
      }

      final result = await installApp(deviceId: deviceId, apkPath: apkPath);
      if (result.exitCode != 0) {
        errs.add('install $device failed: ${result.stderr}');
      }

      final result2 = await launchApp(deviceId: deviceId, url: url);
      if (result2.exitCode != 0) {
        errs.add('launch $device failed: ${result2.stderr}');
      }

      final launchResult = results['部署状态'] as Map? ?? {};
      results['部署状态'] = {
        device: 'ok',
        ...launchResult,
      };
    }

    if (errs.isNotEmpty) {
      print(BotIface(error: errs.join('\n'), results: results));
      exit(1);
    }
  }

  print(BotIface(results: results));
}

Future<String> get lanIP async {
  final result = await Process.run(
    'sh',
    [
      '-c',
      'ip addr | grep -oP "$lanIpReg"',
    ],
    runInShell: true,
  );
  if (result.exitCode != 0) {
    print(BotIface(error: result.stderr.toString()));
    exit(1);
  }
  final ips = (result.stdout as String).split('\n');
  ips.removeWhere((element) => element.isEmpty || element.endsWith('.1'));
  if (ips.isEmpty) {
    print(BotIface(error: 'lan ip not found'));
    exit(1);
  }
  return ips.first.trim();
}

Future<ProcessResult> installApp({
  required String deviceId,
  required String apkPath,
}) async {
  return await Process.run(
    'adb',
    [
      '-s',
      deviceId,
      'install',
      '-r',
      apkPath,
    ],
    runInShell: true,
  );
}

Future<ProcessResult> launchApp({
  required String deviceId,
  required String url,
}) async {
  if (pkgName == null) {
    return await Process.run(
      'adb',
      [
        '-s',
        deviceId,
        'shell',
        'am',
        'start',
        '-a',
        'android.intent.action.VIEW',
        '-d',
        url,
      ],
      runInShell: true,
    );
  }
  return await Process.run(
    'adb',
    [
      '-s',
      deviceId,
      'shell',
      'am',
      'start',
      '-n',
      '$pkgName/.MainActivity',
    ],
    runInShell: true,
  );
}

/// inet 192.168
const lanIpReg = r'(?<=inet\s)192\.168(\.\d+){2}';

/// name: id
const adbIdNameMap = {
  'fjy': 'adb-8H99VCLNJRRGZX9D-A3hLof._adb-tls-connect._tcp',
};

const ENCODER = JsonEncoder.withIndent('  ');
